/*
 * lombo_wdt.c - Driver of LomboTech Watchdog
 *
 * Copyright (C) 2016-2020, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#include "lombo_wdt.h"

static int tmr_margin = LOMBO_WATCHDOG_DEFAULT_TIME;
static bool nowayout = WATCHDOG_NOWAYOUT;

module_param(tmr_margin, int,  0);
module_param(nowayout,   bool, 0);

MODULE_PARM_DESC(tmr_margin, "Watchdog tmr_margin in seconds. (default="
		__MODULE_STRING(LOMBO_WATCHDOG_DEFAULT_TIME) ")");
MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default="
		__MODULE_STRING(WATCHDOG_NOWAYOUT) ")");

static DEFINE_SPINLOCK(lombo_wdt_lock);
static void __iomem *restart_wdt_base;

/**
 * struct lombo_watchdog - Runtime control data for watchdog.
 * @base_addr: pointer to virtual address of watchdog.
 * @wdt_dev: watchdog device for wachdog core use.
 */
struct lombo_watchdog {
	void __iomem *base_addr;
	struct mutex feeddog_lock;
	struct watchdog_device wdt_dev;
};

/**
 * lombo_wdt_hw_init - init the watchdog device.
 * @lombo_wdt: pointer to the lombo watchdog device.
 *
 * Retrun 0 if success.
 */
static int lombo_wdt_hw_init(void *base)
{
	int cnt;

	if (csp_wdt_get_status(base) != 0) {
		csp_wdt_unlock_reg(base);
		csp_wdt_disable(base);
		csp_wdt_lock_reg(base);
		usleep_range(1000, 2000);
	}

	if (csp_wdt_get_int_pend(base) != 0) {
		csp_wdt_unlock_reg(base);
		csp_wdt_clr_int_pend(base);
		csp_wdt_lock_reg(base);
		cnt = 0;
		while (csp_wdt_get_int_pend(base) != 0) {
			if (cnt++ > 30) {
				return -EINVAL;
			}
			udelay(10);
		}
	}

	csp_wdt_unlock_reg(base);
	csp_wdt_init_register(base);
	csp_wdt_lock_reg(base);

	if (csp_wdt_get_current(base))
		return -EIO;

	return 0;
}

/**
 * lombo_wdt_restart - restart system immediately.
 * @lombo_wdt: pointer to the lombo watchdog device.
 */
void lombo_wdt_restart(enum reboot_mode reboot_mode, const char *cmd)
{
	unsigned long flags = 0;
	int err;

	PRT_INFO("watchdog calls system restart immediately.\n");

	spin_lock_irqsave(&lombo_wdt_lock, flags);

	err = lombo_wdt_hw_init(restart_wdt_base);
	if (err) {
		spin_unlock_irqrestore(&lombo_wdt_lock, flags);
		PRT_ERR("cannot reset hardware(%d)\n", err);
		return;
	}

	/* unlock reg */
	csp_wdt_unlock_reg(restart_wdt_base);
	/* set store/ restart_store default */
	csp_wdt_set_reboot_time(restart_wdt_base);
	csp_wdt_set_response_mod(restart_wdt_base,
		LOMBO_WDOG_FIRST_INTERRUPT_THEN_RESET_RESP_MOD);

	/* enable watchdog */
	csp_wdt_enable(restart_wdt_base);

	csp_wdt_lock_reg(restart_wdt_base);
	spin_unlock_irqrestore(&lombo_wdt_lock, flags);

	mdelay(3000);
	PRT_ERR("system restart timeout\n");
}

/**
 * lombo_wdt_stop - stopping the watchdog device.
 * @wdd: pointer to the watchdog device.
 *
 * Retrun 0 if success.
 */
static int lombo_wdt_stop(struct watchdog_device *wdd)
{
	struct lombo_watchdog *lombo_wdt = watchdog_get_drvdata(wdd);
	unsigned long flags = 0;

	PRT_DBG("enter\n");

	spin_lock_irqsave(&lombo_wdt_lock, flags);
	csp_wdt_unlock_reg(lombo_wdt->base_addr);

	if (csp_wdt_get_status(lombo_wdt->base_addr) != 0)
		csp_wdt_disable(lombo_wdt->base_addr);

	csp_wdt_lock_reg(lombo_wdt->base_addr);
	spin_unlock_irqrestore(&lombo_wdt_lock, flags);

	usleep_range(1000, 2000);

	if (csp_wdt_get_current(lombo_wdt->base_addr)) {
		PRT_ERR("stop failed!\n");
		return -EIO;
	}

	return 0;
}

/**
 * lombo_wdt_start - starting the watchdog device.
 * @wdd: pointer to the watchdog device.
 *
 * Retrun 0 if success.
 */
static int lombo_wdt_start(struct watchdog_device *wdd)
{
	int ret = 0;
	unsigned long flags = 0;
	struct lombo_watchdog *lombo_wdt = watchdog_get_drvdata(wdd);

	PRT_DBG("enter\n");

	spin_lock_irqsave(&lombo_wdt_lock, flags);
	csp_wdt_unlock_reg(lombo_wdt->base_addr);

	if (csp_wdt_get_status(lombo_wdt->base_addr) != 0) {
		ret = -EBUSY;
		goto out;
	}

	csp_wdt_clk(lombo_wdt->base_addr, LOMBO_WDOG_CLK_SRC_32K_HARDCODED_DIVIDED_BY_HFEOSC);
	csp_wdt_set_response_mod(lombo_wdt->base_addr,
		LOMBO_WDOG_FIRST_INTERRUPT_THEN_RESET_RESP_MOD);
	csp_wdt_set_timeout(lombo_wdt->base_addr, wdd->timeout);
	csp_wdt_enable(lombo_wdt->base_addr);
out:
	csp_wdt_lock_reg(lombo_wdt->base_addr);
	spin_unlock_irqrestore(&lombo_wdt_lock, flags);

	if (ret)
		PRT_ERR("errno:%d\n", ret);

	return ret;
}

/**
 * lombo_wdt_keepalive - sends a keepalive ping to the watchdog device.
 * @wdd: pointer to the watchdog device.
 *
 * Retrun 0 if success.
 */
static int lombo_wdt_keepalive(struct watchdog_device *wdd)
{
	struct lombo_watchdog *lombo_wdt = watchdog_get_drvdata(wdd);
	unsigned long flags = 0;

	PRT_DBG("enter\n");

	if (csp_wdt_get_status(lombo_wdt->base_addr) == 0)
		return -EINVAL;

	mutex_lock(&lombo_wdt->feeddog_lock);
	spin_lock_irqsave(&lombo_wdt_lock, flags);

	csp_wdt_unlock_reg(lombo_wdt->base_addr);
	/* restart watchdog */
	csp_wdt_restart(lombo_wdt->base_addr);
	csp_wdt_lock_reg(lombo_wdt->base_addr);

	spin_unlock_irqrestore(&lombo_wdt_lock, flags);

	usleep_range(1000, 2000);
	mutex_unlock(&lombo_wdt->feeddog_lock);

	return 0;
}

/**
 * lombo_wdt_set_timeout - setting the watchdog devices timeout value.
 * @wdd: pointer to the watchdog device.
 * @timeout: timeout value (n7v1:seconds; others:milliseconds).
 *           if 0 system will restart immediately.
 *
 * Retrun 0 if success.
 */
static int lombo_wdt_set_timeout(struct watchdog_device *wdd,
				 unsigned int timeout)
{
	unsigned long flags = 0;

	PRT_DBG("enter, timeout=%d\n", timeout);

	if (watchdog_active(wdd))
		PRT_WARN("watchdog is active, please stop, and new timeout will be apply by WDIOS_ENABLECARD\n");

	spin_lock_irqsave(&lombo_wdt_lock, flags);
	/* update timeout in watchdog device */
	wdd->timeout = timeout;

	spin_unlock_irqrestore(&lombo_wdt_lock, flags);

	return 0;
}

static unsigned int lombo_wdt_get_timeleft(struct watchdog_device *wdd)
{

	struct lombo_watchdog *lombo_wdt = watchdog_get_drvdata(wdd);
	unsigned int value = 0;
	unsigned long flags = 0;

	spin_lock_irqsave(&lombo_wdt_lock, flags);
	csp_wdt_get_timeleft(lombo_wdt->base_addr, &value);
	spin_unlock_irqrestore(&lombo_wdt_lock, flags);

	return value;
}

static const struct watchdog_info lombo_wdt_info = {
	.options  = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE,
	.firmware_version = 1,
	.identity = "Lombo Watchdog",
};

static struct watchdog_ops lombo_wdt_ops = {
	.owner = THIS_MODULE,
	.start = lombo_wdt_start,
	.stop = lombo_wdt_stop,
	.ping = lombo_wdt_keepalive,
	.set_timeout = lombo_wdt_set_timeout,
	.get_timeleft = lombo_wdt_get_timeleft,
};

static int lombo_wdt_probe(struct platform_device *pdev)
{
	struct lombo_watchdog *lombo_wdt;
	struct resource *res;
	int err = 0;

	lombo_wdt = kzalloc(sizeof(*lombo_wdt), GFP_KERNEL);
	if (!lombo_wdt) {
		PRT_ERR("fail to allocate memory\n");
		err = -ENOMEM;
		goto out;
	}

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (res == NULL) {
		PRT_ERR("no memory resource specified\n");
		err = -ENOENT;
		goto out_kfree;
	}

	/* get the base address for the watchdog timer */
	lombo_wdt->base_addr = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(lombo_wdt->base_addr)) {
		PRT_ERR("fail to ioremap\n");
		err = PTR_ERR(lombo_wdt->base_addr);
		goto out_kfree;
	}

	err = lombo_wdt_hw_init(lombo_wdt->base_addr);
	if (err) {
		PRT_ERR("cannot reset watchdog hardware(%d)\n", err);
		goto out_kfree;
	}

	mutex_init(&lombo_wdt->feeddog_lock);

	lombo_wdt->wdt_dev.ops = &lombo_wdt_ops;
	lombo_wdt->wdt_dev.info = &lombo_wdt_info;
	lombo_wdt->wdt_dev.timeout = LOMBO_WATCHDOG_DEFAULT_TIME;
	lombo_wdt->wdt_dev.max_timeout = LOMBO_WATCHDOG_MAX_TIMEOUT;
	lombo_wdt->wdt_dev.min_timeout = LOMBO_WATCHDOG_MIN_TIMEOUT;
	lombo_wdt->wdt_dev.parent = &pdev->dev;

	platform_set_drvdata(pdev, lombo_wdt);
	watchdog_set_drvdata(&lombo_wdt->wdt_dev, lombo_wdt);

	watchdog_init_timeout(&lombo_wdt->wdt_dev,
			tmr_margin, &pdev->dev);
	watchdog_set_nowayout(&lombo_wdt->wdt_dev, nowayout);

	/* register watchdog device */
	err = watchdog_register_device(&lombo_wdt->wdt_dev);
	if (err) {
		PRT_ERR("cannot register watchdog (%d)\n", err);
		goto out_kfree;
	}

	restart_wdt_base = lombo_wdt->base_addr;

	PRT_INFO("lombo watchdog registered (timeout: %d, nowayout: %d)\n",
		lombo_wdt->wdt_dev.timeout, nowayout);

	return 0;

out_kfree:
	PRT_ERR("lombo watchdog probe catch error:%d\n", err);
	kfree(lombo_wdt);
out:
	return err;
}

static int lombo_wdt_remove(struct platform_device *pdev)
{
	struct lombo_watchdog *lombo_wdt = platform_get_drvdata(pdev);
	int err;

	PRT_DBG("enter\n");

	err = lombo_wdt_hw_init(lombo_wdt->base_addr);
	if (err) {
		PRT_ERR("cannot reset hardware(%d)\n", err);
		return -EIO;
	}

	watchdog_unregister_device(&lombo_wdt->wdt_dev);
	watchdog_set_drvdata(&lombo_wdt->wdt_dev, NULL);
	kfree(lombo_wdt);

	return 0;
}

static void lombo_wdt_shutdown(struct platform_device *pdev)
{
	struct lombo_watchdog *lombo_wdt = platform_get_drvdata(pdev);

	PRT_DBG("enter\n");

	lombo_wdt_stop(&lombo_wdt->wdt_dev);
}

static const struct of_device_id lombo_wdt_match[] = {
	{ .compatible = "lombo,n5-wdt" },
	{ .compatible = "lombo,n7-wdt" },
	{ .compatible = "lombo,n9v1-wdt" },
	{ .compatible = "lombo,n9v3-wdt" },
	{ .compatible = "lombo,wdt" },
	{},
};
MODULE_DEVICE_TABLE(of, lombo_wdt_match);

static struct platform_driver lombo_wdt_driver = {
	.driver	= {
		.owner = THIS_MODULE,
		.name = "lombo-wdt",
		.of_match_table = lombo_wdt_match,
	},
	.probe = lombo_wdt_probe,
	.remove = lombo_wdt_remove,
	.shutdown = lombo_wdt_shutdown,
};
module_platform_driver(lombo_wdt_driver);

MODULE_AUTHOR("lomboswer@lombotech.com>");
MODULE_DESCRIPTION("Watchdog Driver for Lombo SoCs");
MODULE_LICENSE("GPL");
